﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;
using System.Configuration;
using System.Threading.Tasks;
using System.Runtime.Serialization;
using MovieRental.GeneralDataExceptions;
using MovieRental.XmlDataService;
using MovieRental.XmlDataProviderCore.Configuration;

namespace MovieRental.XmlDataProviderCore
{
    public class XmlDataProvider : IDisposable
    {
        private Task _updateTask;
        private bool _disposed;
        private InitializationMode _mode;
        private const string _defaultDirectory = "Data";
        private DateTime _lastUpdate = DateTime.Now;
        private TimeSpan _minUpdateInterval = new TimeSpan(0, 0, 3);
        private HashSet<string> _tablesToUpdate = new HashSet<string>();
        private HashSet<string> _updateQueue = new HashSet<string>();

        internal Dictionary<string, XDocument> _tables;

        internal XmlDataModel Model { private set; get; }

        private void InitializeDirectory(string workingDirectory, bool creating = false)
        {
            DirectoryInfo dir = new DirectoryInfo(workingDirectory);
            if (!dir.Exists && creating == false)
                throw new DbConnectionException("The working directory doesn't exist or empty.", null);
            if (creating == true)
            {
                try
                {
                    dir.Create();
                }
                catch (Exception ex)
                {
                    throw new DbConnectionException("Cannot create the working directory. ", ex);
                }
            }
            WorkingDirectory = workingDirectory;
        }

        private XmlDataProvider()
        {
            _tables = new Dictionary<string, XDocument>();
            Model = new XmlDataModel();
        }

        ~XmlDataProvider()
        {
            Dispose();
        }

        public void Dispose()
        {
            if (!_disposed)
            {
                Sync(true);
                _disposed = true;
            }
        }

        public string WorkingDirectory { private set; get; }

        public static XmlDataProvider Initialize(string workingDirectory, InitializationMode mode)
        {
            XmlDataProvider provider = new XmlDataProvider();
            provider._mode = mode;
            if (mode == InitializationMode.Open)
                provider.InitializeDirectory(workingDirectory);
            else
                provider.InitializeDirectory(workingDirectory, true);
            return provider;
        }

        public static XmlDataProvider Initialize()
        {
            XmlDataProviderConfiguration config = (XmlDataProviderConfiguration) ConfigurationManager.GetSection("xmlDataProviderConfiguration");
            if (config != null)
                return Initialize(config.WorkingDirectory, config.Mode);
            else
                return Initialize(_defaultDirectory, InitializationMode.DropCreate);  
        }

        private void CreateTable(string tableName)
        {
            string fileName = Path.Combine(WorkingDirectory, tableName + ".xml");
            XDocument doc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), new XElement(tableName));
            try
            {
                doc.Save(fileName);
                _tables.Add(tableName, doc);
            }
            catch (Exception ex)
            {
                throw new DbConnectionException("Can't create table file.", ex);
            }
        }

        private void LoadTable(string tableName)
        {
            string fileName = Path.Combine(WorkingDirectory, tableName + ".xml");
            XDocument doc;
            try
            {
                doc = XDocument.Load(fileName);
                _tables.Add(tableName, doc);
            }
            catch (FileNotFoundException)
            {
                CreateTable(tableName);
            }
            catch (Exception ex)
            {
                throw new DbConnectionException("Can't load table file.", ex);
            }
        }

        private void ProcessEntity(object entity)
        {
            ProcessEntity(entity.GetType());
        }

        private void ProcessEntity(Type entityType)
        {
            string tableName = Model.GetEntityInfo(entityType).TableName;
            if (!_tables.ContainsKey(tableName))
            {
                if (_mode == InitializationMode.DropCreate)
                    CreateTable(tableName);
                else
                    LoadTable(tableName);
            }
        }

        private void UpdateProcess()
        {
            foreach (var tableName in (_tablesToUpdate as IEnumerable<string>))
            {
                string fileName = Path.Combine(WorkingDirectory, tableName + ".xml");
                _tables[tableName].Save(fileName);
            }
            _tablesToUpdate.Clear();
        }

        private void Sync(bool force = false)
        {
            if (_updateTask != null)
                _updateTask.Wait();
            if (force == true || DateTime.Now - _lastUpdate > _minUpdateInterval)
            {
                _tablesToUpdate.UnionWith(_updateQueue);
                _updateQueue.Clear();
                _updateTask = new Task(UpdateProcess);
                _updateTask.Start();
                _lastUpdate = DateTime.Now;
            }
        }

        internal void UpdateTables(IEnumerable<string> tableNames)
        {
            _updateQueue.UnionWith(tableNames);
        }


        internal XElement GetRootForEntity(object entity)
        {
            ProcessEntity(entity);
            return _tables[Model.GetEntityInfo(entity).TableName].Root;
        }

        internal XElement GetRootForEntity(Type entityType)
        {
            ProcessEntity(entityType);
            return _tables[Model.GetEntityInfo(entityType).TableName].Root;
        }

        internal string GetIdElementNameForEntity(object entity)
        {
            ProcessEntity(entity);
            return Model.GetEntityInfo(entity).PrimaryKeyAttributeName;
        }

        internal string GetTableNameForEntity(object entity)
        {
            ProcessEntity(entity);
            return Model.GetEntityInfo(entity).TableName;
        }

        public XmlDataContext CreateContext()
        {
            if (_disposed)
                return null;
            Sync();
            return new XmlDataContext(this);
        }
        
    }
}
